using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;
using System.Net;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.IO.Compression;
using System.Collections.Generic;
using System.Collections;
using System.Security.Cryptography;
using System.Text;
using System.Html.HtmlElements;
using System.Reflection;
using System.Threading.Tasks;
using System.Html.HtmlTags;
namespace System.Html {
    /// <summary>
    /// Container for the parsing of malformed html into xml for data extraction
    /// </summary>
    public class HtmlDocument: XmlDocument {

        static Hashtable compiledTemplateAutoCache = new Hashtable();
        static object lockCacheObject = new object();
        HTTPSiteWrapper siteWrapper = null;
        const string NEWLINE = "\r\n";
        const char BACKSLASH_R = '\r';
        const char BACKSHASH_N = '\n';
        const char EMPTY_WHITE_SPACE = ' ';

        /// <summary>
        /// Set to true to tell the parser to ignore all namespace (xmlns) attributes. Use with caution
        /// </summary>
        public bool IgnoreNamespaces {
            get;
            set;
        }

        /// <summary>
        /// Hashtable of the compiled templates that have been passed in this document thus far
        /// </summary>
        public Hashtable TemplateCache {
            get {
                return compiledTemplateAutoCache;
            }
        }

        public HtmlDocument() {
            document = new Javascript.DOM.Document();
            PostDataEncoding = Encoding.Default;
            AutoCreateTags = false;
        }

        /// <summary>
        /// Tells the document to use a non default encoding
        /// for posting data.
        /// </summary>
        public Encoding PostDataEncoding {
            get;
            set;
        }
        /// <summary>
        /// Returns the uri of the last load request.
        /// this is stored primarily to resolve relative Urls
        /// </summary>
        public Uri LastUriLoad {
            get;
            protected set;
        }

        public bool AutoCreateTags{
            get;
            set;
        }

        public override XmlElement CreateElement(string prefix, string localName, string namespaceURI) {
            if (AutoCreateTags) {
                if (localName != "HTMLDocument") {
                    ConstructorInfo _constructor = HtmlParser.GetTagInstance(localName).GetType().GetConstructor(
                        new Type[] { typeof(String), typeof(String), typeof(String), typeof(HtmlDocument) });
                    return (XmlElement)_constructor.Invoke(new object[] { prefix, localName, namespaceURI, this });
                }
                else {
                    return (XmlElement)typeof(XmlElement).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)[1].Invoke(new object[] { prefix, 
                    localName, namespaceURI, this });
                }
            }
            else {
                return base.CreateElement(prefix, localName, namespaceURI);
            }
        }

        /// <summary>
        /// One method call to dispose of and clear the TemplateCache
        /// </summary>
        public void ClearTemplateCache() {

        }

        #region load methods
        
        /// <summary>
        /// Loads html from a Uri into the object and returns the data from the uri (i.e. html text)
        /// </summary>
        /// <param name="httpLocation">The Uri with the data to load</param>
        /// <param name="wrapper">optionaly use the speficied site wrapper for all requests</param>
        /// <param name="postData">optionally send post information with the request</param>
        /// <returns>The original html data as downloaded from the uri. If not needed, discard</returns>
        public string Load(Uri httpLocation) {
            string _htmlData;
            _htmlData = LoadData( (HttpWebResponse)WebRequest.Create(httpLocation).GetResponse());
            new HtmlParser() { IgnoreNamespaces = IgnoreNamespaces }.Parse(this, _htmlData);
            ///return the downloaded html data; the user may want it
            LastUriLoad = httpLocation;
            return _htmlData;
        }

        private string LoadData(HttpWebResponse _response) {
            const string GZIP_CONTENT_ENCODING = "gzip";
            if (_response == null) {
                return string.Empty;
            }
            StreamReader _strRead;
            string _htmlData;
            ///detect compressed streams and decompress for reading automatically
            if (!String.IsNullOrEmpty(_response.ContentEncoding) && _response.ContentEncoding.ToLower() == GZIP_CONTENT_ENCODING) {
                _strRead = new StreamReader(new GZipStream(_response.GetResponseStream(), CompressionMode.Decompress), System.Text.Encoding.GetEncoding(_response.CharacterSet));
            }
            else {
                ///ensure that you parse in the charset of the data stream.
                _strRead = new StreamReader(_response.GetResponseStream(), System.Text.Encoding.GetEncoding(_response.CharacterSet));
            }
            ///block and wait for the entire data stream. Note exception handling is intentionaly left out
            ///there are a varity of exceptions that can be thrown here
            _htmlData = _strRead.ReadToEnd();
            ///Get parse into xml then load the xml into the object
            return _htmlData;
        }
        /// <summary>
        /// Loads html from a site wrapper an returns the data
        /// </summary>
        /// <param name="httpLocation">the absolute URI to return</param>
        /// <param name="wrapper">The site collection wrapper to use</param>
        /// <param name="postData">optional post data to send.</param>
        /// <returns></returns>
        public string Load(HTTPSiteWrapper wrapper, string postData = "") {
            if (wrapper != null) {
                siteWrapper = wrapper;
                string _htmlData = siteWrapper.Init(false, postData, PostDataEncoding);
                new HtmlParser() { IgnoreNamespaces = IgnoreNamespaces }.Parse(this, _htmlData);
                LastUriLoad = siteWrapper.Home;
                ///return the downloaded html data; the user may want it
                return _htmlData;
            }
            else {
                throw new ApplicationException("The site wrapper cannont be null");
            }
        }

        /// <summary>
        /// Loads Html into the object. Always assumes that the data is html and not XML!
        /// </summary>
        /// <param name="html">Html text to load</param>
        public void LoadHTML(string html) {
            ///load the xml template base
            LoadXml(Properties.Resources.HtmlDocumentBase);
            /// instance the parser and set it working
            HtmlParser _parser = new HtmlParser();
            _parser.IgnoreNamespaces = IgnoreNamespaces;
            _parser.Parse(this, html);
        }
        /// <summary>
        /// Loads the content of the src attributes of all text/javascript content type elements
        /// </summary>
        public void LoadSrcs(bool useParallel) {
            List<XmlNode> _tags = new List<XmlNode>();
            //check for a base uri
            BASE _base = (BASE)SelectSingleNode("//base[@href!='']");

            foreach (XmlNode _node in SelectNodes("//*[@src!='' and @type='text/javascript']")) {
                _tags.Add(_node);
            }
            if (useParallel) {
                Parallel.ForEach(_tags, item => ProcessSrc(item, _base));
            }
            else {
                foreach (XmlNode node in _tags) {
                    ProcessSrc(node, _base);
                }
            }
        }

        private void ProcessSrc(XmlNode tag, BASE baseTag) {
            Uri _httpLocation;
            string _srcValue = tag.Attributes["src"].Value;
            if(!Uri.TryCreate(_srcValue, UriKind.Absolute, out _httpLocation)){
                if(Uri.TryCreate(_srcValue, UriKind.Relative, out _httpLocation)){
                    if (baseTag != null) {
                        _httpLocation = new Uri(new Uri(baseTag.Attributes["href"].Value), _srcValue);
                    }
                    else {
                        _httpLocation = new Uri(LastUriLoad, _srcValue);
                    }
                }
            }
            string _data = string.Empty;
            _data = siteWrapper.GetPage(_httpLocation, string.Empty, "text/javascript", PostDataEncoding);
            tag.InnerText = _data;
        }

        #endregion

        #region transform methods

        /// <summary>
        /// Wrapper for XSLT transformations. Transforms the current DOM into another DOM
        /// </summary>
        /// <param name="xsltTemplate">Text of the XSLT template to load and execute</param>
        /// <returns>HtmlDocument object with the transformed changes</returns>
        public HtmlDocument Transform(string xsltTemplate) {
            ///get the transform from cache
            XslCompiledTransform _transform = CacheTemplate(xsltTemplate);

            ///prepare the output stream
            MemoryStream _stream = new MemoryStream();
            XmlWriterSettings _writerSettings = new XmlWriterSettings();
            _writerSettings.ConformanceLevel = ConformanceLevel.Auto;
            XmlWriter _writer = XmlWriter.Create(_stream,_writerSettings);

            ///execute the transform
            _transform.Transform(this, _writer);
            _writer.Flush();

            ///even though the output document is xml, we're still 
            ///loading it in an HtmlDocument for convience. The user may
            ///want to perform another transform on that document
            ///and this way it's more handy. Since the HtmlDocument is a
            ///XmlDocument it can be cast as XmlDocument if so desired
            HtmlDocument _hDoc = new HtmlDocument();
            _stream.Seek(0, SeekOrigin.Begin);
            _hDoc.Load(_stream);
            ///and return
            return _hDoc;
        }

        /// <summary>
        /// Causes an XSLT template to be cached for future use by the object
        /// </summary>
        /// <param name="xsltTemplate"></param>
        /// <returns></returns>
        public XslCompiledTransform CacheTemplate(string xsltTemplate) {
            ///determine if the tempalte has already been cached
            MD5 _md5Sig = MD5.Create();
            ///compute the md5 hash of the template
            string _sig = Convert.ToBase64String(_md5Sig.ComputeHash(System.Text.Encoding.ASCII.GetBytes(xsltTemplate)));
            lock (lockCacheObject) {
                if (!TemplateCache.ContainsKey(_sig)) {
                    ///create a memory stream to hold the template
                    MemoryStream _templateStream = new MemoryStream();
                    StreamWriter _strWriter = new StreamWriter(_templateStream);
                    _strWriter.Write(xsltTemplate);
                    _strWriter.Flush();
                    ///reset the seek location
                    _templateStream.Seek(0, SeekOrigin.Begin);
                    ///setup the template to be executed
                    XPathDocument _sheet = new XPathDocument(_templateStream);

                    ///so the compiled form can actually be saved for later use.
                    XslCompiledTransform _transform = new XslCompiledTransform(true);

                    ///load the sheet
                    _transform.Load(_sheet);

                    ///and store it in the cache
                    TemplateCache.Add(_sig, _transform);

                    ///and return it if the user wants
                    return _transform;
                }
                else {
                    try {
                        XslCompiledTransform _transform = (XslCompiledTransform)TemplateCache[_sig];
                        ///return it since it's most likely what the user wantes
                        return _transform;
                    }
                    catch (Exception ex) {
                        ///box and throw the exception
                        throw new ApplicationException("Unable to obtain the template from cache", ex);
                    }
                }
            }
        }

        #endregion
        /// <summary>
        /// Returns the xml representation of the class, in a single method call. Also
        /// does the handy job of removing the CDATA sections.
        /// </summary>
        /// <returns>html version of the class</returns>
        public string GetHtml() {
            HtmlFormatter _formater = new HtmlFormatter();
            StringBuilder _htmlStringBuilder = new StringBuilder();
            string _text = string.Empty;
            foreach (XmlNode _node in DocumentElement.ChildNodes) {
                _formater.Format(_node, _htmlStringBuilder);
            }
            return _htmlStringBuilder.ToString();
        }

        /// <summary>
        /// Uses Microsoft's html to xaml convertor to produce flow document xaml
        /// </summary>
        /// <returns></returns>
        public string GetXaml() {
            return HtmlToXamlConverter.ConvertHtmlToXaml(this, true);
        }

        #region Normalize method proposed by RudolfHenning [of Codeplex]
        /// <summary>
        /// Formats the html document tags by indenting tags depending on the depth of the tag
        /// </summary>
        public override void Normalize() {
            foreach (XmlNode node in DocumentElement.ChildNodes) {
                NormalizeNode(node, -1);
            }
            base.Normalize();
        }

        /// <summary>
        /// Recursive normalization method helper
        /// </summary>
        /// <param name="node">current node</param>
        /// <param name="nestedLevel">depth of node</param>
        private void NormalizeNode(XmlNode node, int nestedLevel) {
            int _nestedLevelBy2 = 2 * nestedLevel;
            if (node.NodeType != XmlNodeType.CDATA) {
                if (nestedLevel >= 0) {
                    if (node.PreviousSibling != null) {
                        if (node.PreviousSibling.NodeType == XmlNodeType.CDATA) {
                            node.PreviousSibling.Value = node.PreviousSibling.Value.TrimEnd(EMPTY_WHITE_SPACE, BACKSLASH_R, BACKSHASH_N);
                            node.PreviousSibling.Value += NEWLINE + new string(EMPTY_WHITE_SPACE, _nestedLevelBy2);
                        }
                        else {
                            XmlCDataSection cdata = CreateCDataSection(NEWLINE + new string(EMPTY_WHITE_SPACE, _nestedLevelBy2));
                            node.InsertBefore(cdata, node);
                        }
                    }
                    else {
                        XmlCDataSection cdata = CreateCDataSection(NEWLINE + new string(EMPTY_WHITE_SPACE, _nestedLevelBy2));
                        node.ParentNode.InsertBefore(cdata, node);
                    }
                }
                if (node.NextSibling == null) {
                    XmlCDataSection cdata = CreateCDataSection(NEWLINE + new string(EMPTY_WHITE_SPACE, _nestedLevelBy2));
                    node.ParentNode.InsertAfter(cdata, node);
                }

                foreach (XmlNode childnode in node.ChildNodes) {
                    NormalizeNode(childnode, nestedLevel + 1);
                }
            }
            else {
                if (nestedLevel > 1) {
                    if (node.Value == null) {
                        node.Value = string.Empty;
                    }
                    node.Value = node.Value.TrimEnd(EMPTY_WHITE_SPACE, BACKSLASH_R, BACKSHASH_N);
                    if (node.Value.Length == 0) {
                        int _whiteSpaceCalculation = 2 * (nestedLevel - 1);
                        node.Value += NEWLINE + new string(EMPTY_WHITE_SPACE, _whiteSpaceCalculation);
                    }
                }
            }
        }
        #endregion

        #region Javacript specific functionality

        public Javascript.DOM.Document document {
            get;
            private set;
        }
        public Javascript.Browser.Location location {
            get;
            private set;
        }
        public Javascript.Browser.Navigator navigator {
            get;
            private set;
        }
        public Javascript.Browser.Screen screen {
            get;
            private set;
        }
        public Javascript.Browser.Window window {
            get;
            private set;
        }
        #endregion
    }
}
